/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Save File to.
 *
 * The Initial Developer of the Original Code is
 * Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2009
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */


var EXPORTED_SYMBOLS =
[
    "CascadeMenu",
];

const Cu = Components.utils;

Cu.import("resource://savefileto/common.jsm");
Cu.import("resource://savefileto/Heap.jsm");


function CascadeMenu(config)
{
    this.config = config;
}

CascadeMenu.prototype =
{
    attach: function CM_attach(menu)
    {
        var self = this;
        var doc = menu.ownerDocument;
        
        var menuPopup = this.createElement(doc, "menupopup", {class: this.config.mainClassName, position: this.config.mainMenuPosition});
        menuPopup.addEventListener("popupshowing", function(e) { self.onPopupShowing(e); }, false);
        menuPopup.addEventListener("popuphidden", function(e) { self.onPopupHidden(e); }, false);
        menu.appendChild(menuPopup);
        
        menuPopup.addEventListener("command", function(e) { self.onCommand(e); }, false);
    },
    
    onPopupShowing: function CM_onPopupShowing(e)
    {
        e.stopPropagation();
        var menuPopup = e.originalTarget;
        var entry = menuPopup.parentNode.entry;
        
        // main menu
        if (! entry)
        {
            // popup position in ui is determined before popupshowing event
            // so if position has been changed, popup opening must be cancelled,
            // position updated and then menu can be re-opened
            var position = this.config.mainMenuPosition;
            if (position && (position != menuPopup.position))
            {
                e.preventDefault();
                menuPopup.position = position;
                callLater(0, null, function() { menuPopup.parentNode.open = true; });
                return;
            }
            
            this.removeChildren(menuPopup);
            this.populateMainMenu(menuPopup);
        }
        // sub menu
        else
        {
            if (! menuPopup.firstChild)
                this.populateSubMenu(menuPopup, entry);
        }
    },
    
    onPopupHidden: function CM_onPopupHidden(e)
    {
        e.stopPropagation();
        var menuPopup = e.originalTarget;
        var entry = menuPopup.parentNode.entry;
        
        // main menu
        if (! entry)
            this.removeTimer = callLater(0, this, this.removeChildren, [menuPopup]);
    },
    
    onCommand: function CM_onCommand(e)
    {
        var callback = this.config.commandCallback;
        var entry = e.originalTarget.entry;
        callback(e, entry, this);
    },
    
    removeChildren: function CM_removeChildren(element)
    {
        if (this.removeTimer)
        {
            this.removeTimer.cancel();
            this.removeTimer = null;
        }
        
        var child;
        while ((child = element.lastChild) != null)
            element.removeChild(child);
    },
    
    populateMainMenu: function CM_populateMainMenu(menuPopup)
    {
        var entries = this.config.dataSource.entries;
        
        for each (var entry in entries)
            this.appendMenuEntry(menuPopup, entry);
    },
    
    populateSubMenu: function CM_populateSubMenu(menuPopup, entry)
    {
        var children = entry.children;
        
        if (! entry.compacted)
        {
            this.appendMenuItem(menuPopup, entry, this.config.commandLabel);
            
            if (children.length == 0)
                return;
            
            this.appendMenuSeparator(menuPopup);
        }
        
        var minMenuLength = Math.max(2, this.config.minMenuLength);
        var maxMenuLength = this.config.maxMenuLength;
        if (maxMenuLength > 0)
            children = this.compact(children, minMenuLength, maxMenuLength);
        
        for each (var child in children)
            this.appendMenuEntry(menuPopup, child);
    },
    
    compact: function CM_compact(entries, minLength, maxLength)
    {
        maxLength = Math.max(minLength, maxLength);
        
        if (entries.length <= maxLength)
            return entries;
        
        var prefixEntryList = [{prefix: entry.labelLower, entries: [entry]} for each (entry in entries)];
        var result = [prefixEntryList[0]];
        
        // heap for entries common name prefixes (order: longest prefix, alphabetic)
        var reNonAlphanumEnd = /[_\W]+$/;
        var trim = function(s) { return s.replace(reNonAlphanumEnd, ""); };
        var compare = function(a, b) { var d = a.length - b.length; return ((d != 0) ? d : ((b < a) ? -1 : 1)); };
        var prefixHeap = new Heap(compare);
        
        for (var i = 1; i < prefixEntryList.length; i++)
        {
            var prefixEntry = prefixEntryList[i];
            var lastPrefixEntry = result[result.length - 1];
            var prefix = trim(commonPrefix(lastPrefixEntry.prefix, prefixEntry.prefix));
            
            // if last prefixEntry has a sub-list and its prefix equals current, just add the current to the sub-list
            if ((lastPrefixEntry.entries.length > 1) && (prefix == lastPrefixEntry.prefix))
            {
                lastPrefixEntry.entries.push(prefixEntry.entries[0]);
                continue;
            }
            
            result.push(prefixEntry);
            prefixHeap.push(prefix);
            
            // start compacting result list when it grows larger than maxLength
            if (result.length <= maxLength)
                continue;
            
            // compact entries with the longest common prefix into a sub-list, if there is such a prefix
            prefix = prefixHeap.peek();
            if (prefix.length == 0)
                continue;
            
            var resultLength = result.length;
            var prefixLength = prefix.length;
            
            // search entry range that match the longest common prefix
            var lo = 0;
            while ((lo < resultLength) && (result[lo].prefix.substr(0, prefixLength) != prefix))
                lo++;
            var hi = lo + 1;
            while ((hi < resultLength) && (result[hi].prefix.substr(0, prefixLength) == prefix))
                hi++;
            
            // don't allow too aggressive compacting
            var n = hi - lo - 1;
            if ((resultLength - n) < minLength)
                continue;
            
            // combine entries in result[lo,hi[ into a single entry result[lo]
            var subEntries = result[lo].entries;
            var subResult = result.splice(lo + 1, n);
            for (var k = 0; k < subResult.length; k++)
                subEntries = subEntries.concat(subResult[k].entries);
            result[lo] = {prefix: prefix, entries: subEntries};
            
            // remove all instances of the prefix from the heap
            do { prefixHeap.pop(); }
            while ((prefixHeap.length > 0) && (prefixHeap.peek() == prefix))
            
            // add new common prefixes to heap
            if (lo > 0)
                prefixHeap.push(trim(commonPrefix(prefix, result[lo - 1].prefix)));
            if (lo + 1 < result.length)
                prefixHeap.push(trim(commonPrefix(prefix, result[lo + 1].prefix)));
        }
        
        // convert prefixEntries back to normal entries
        entries = [];
        for each (var prefixEntry in result)
            if (prefixEntry.entries.length == 1)
                entries.push(prefixEntry.entries[0]);
            else
                entries.push({type: "menu", label: prefixEntry.prefix + "\u2026", hasChildren: true, children: prefixEntry.entries, compacted: true});
        
        return entries;
    },
    
    appendMenuEntry: function CM_appendMenuEntry(menuPopup, entry)
    {
        var type = entry.type || "item";
        
        switch (type)
        {
            case "menu":
                if (this.config.trimEmptyMenu && (("hasChildren" in entry) && (! entry.hasChildren)))
                    return this.appendMenuItem(menuPopup, entry);
                else
                    return this.appendMenu(menuPopup, entry);
                
            case "item":
                return this.appendMenuItem(menuPopup, entry);
                
            case "separator":
                return this.appendMenuSeparator(menuPopup);
                
            default:
                Cu.reportError(new Error("invalid entry type: " + type + " " + entry));
                return null;
        }
    },
    
    appendMenu: function CM_appendMenu(menuPopup, entry)
    {
        var self = this;
        var doc = menuPopup.ownerDocument;
        
        var subMenuPopup = this.createElement(doc, "menupopup", {class: this.config.subClassName, position: this.getSubMenuPosition(menuPopup)});
        
        var subMenu = this.createElement(doc, "menu", {label: entry.label}, [subMenuPopup]);
        subMenu.entry = entry;
        
        menuPopup.appendChild(subMenu);
        
        return subMenu;
    },
    
    appendMenuItem: function CM_appendMenuItem(menuPopup, entry, label)
    {
        var self = this;
        var doc = menuPopup.ownerDocument;
        
        label = label || entry.label;
        var menuItem = this.createElement(doc, "menuitem", {label: label, command: entry.command});
        menuItem.entry = entry;
        
        menuPopup.appendChild(menuItem);
        
        return menuItem;
    },
    
    appendMenuSeparator: function CM_appendMenuSeparator(menuPopup)
    {
        var doc = menuPopup.ownerDocument;
        
        var menuSeparator = this.createElement(doc, "menuseparator");
        menuPopup.appendChild(menuSeparator);
        
        return menuSeparator;
    },
    
    createElement: function CM_createElement(doc, name, attrs, children)
    {
        var element = doc.createElement(name);
        
        if (attrs)
            for (let [attrName, attrValue] in Iterator(attrs))
                if (attrValue !== undefined)
                    element.setAttribute(attrName, attrValue);
        
        if (children)
            for each (var child in children)
                element.appendChild(child);
        
        return element;
    },
    
    alternateMenuPosition:
    {
        start_before: "end_before",
        start_after: "end_after",
        end_before: "start_before",
        end_after: "start_after",
    },
    
    getSubMenuPosition: function(menuPopup)
    {
        if (this.config.alternatingMenuPosition)
            if (menuPopup.position in this.alternateMenuPosition)
                return this.alternateMenuPosition[menuPopup.position];
        
        return this.config.subMenuPosition;
    }
}
